首先老师谈为什么要在react中使用typescript,因为ts能提供很好的代码提示和维护体验。

关于这个课程:

创建项目npm create vite@latest react-typescript-demo,将App.tsx里面清理一下,方便后面的学习。
xxxxxxxxxx91// App.tsx23import "./App.css";45function App() {6 return <></>;7}89export default App;Typing Props,这里的typing是指加上类型,完整翻译就是给props(属性)加上类型。不是名词。
老师推荐使用type来定义ts类型,interface用在专门的定义文件里面比较多。
创建文件src\components\Greet.tsx,在App.tsx里面引用,引用的同时传入参数。这时候在Greet组件里面就需要定义ts类型。
xxxxxxxxxx141// mycode\react-typescript-demo\src\components\Greet.tsx23// 定义props类型4type GreetProps = {5 name: string;6};78export const Greet = (props: GreetProps) => {9 return (10 <div>11 <h2>Welcome {props.name}, You have 10 unread messages.</h2>12 </div>13 );14};xxxxxxxxxx141// src/App.tsx23import "./App.css";4import { Greet } from "./components/Greet";56function App() {7 return (8 <>9 <Greet name="tom anderson" />10 </>11 );12}1314export default App;效果:

基础属性。
这节课主要学习不同ts类型的props属性,number、boolean、object、array、
xxxxxxxxxx1001// src/App.tsx23import "./App.css";4import { Greet } from "./components/Greet";5import { Person } from "./components/Person";6import { PersonList } from "./components/PersonList";78function App() {9 const person = {10 first: "tom",11 last: "anderson",12 };1314 const names = [15 {16 first: "jack",17 last: "bruce",18 },19 {20 first: "nick",21 last: "harrison",22 },23 {24 first: "bruce",25 last: "wayne",26 },27 ];28 return (29 <>30 <Greet name="tom anderson" messageCount={20} isLoggedIn={false} />31 <Person person={person} />32 <PersonList names={names} />33 </>34 );35}3637export default App;383940// mycode\react-typescript-demo\src\components\Greet.tsx41// 定义props类型42type GreetProps = {43 name: string;44 messageCount: number;45 isLoggedIn: boolean;46};4748export const Greet = (props: GreetProps) => {49 return (50 <div>51 {props.isLoggedIn ? (52 <h2>53 Welcome {props.name}, You have {props.messageCount} unread messages.54 </h2>55 ) : (56 <h2>Welcome Guest</h2>57 )}58 </div>59 );60};616263// mycode\react-typescript-demo\src\components\Person.tsx64// 定义对象类型65type PersonProps = {66 person: {67 first: string;68 last: string;69 };70};7172export const Person = (props: PersonProps) => {73 return (74 <div>75 {props.person.first} {props.person.last}76 </div>77 );78};798081// mycode\react-typescript-demo\src\components\PersonList.tsx82// 定义数组类型,更准确一点是定义对象数组类型83type PersonListProps = {84 names: {85 first: string;86 last: string;87 }[];88};8990export const PersonList = (props: PersonListProps) => {91 return (92 <div>93 {props.names.map((name) => (94 <h2 key={name.first}>95 {name.first} {name.last}96 </h2>97 ))}98 </div>99 );100};
高级属性。
使用联合类型,可以规范用户的输入。
xxxxxxxxxx141// App.tsx2import "./App.css";3import { Status } from "./components/Status";45function App() {6 return (7 <>8 {/* 这里的status,在使用时只能输入联合类型规定的字符,有效规范了输入 */}9 <Status status="loading" />10 </>11 );12}1314export default App;xxxxxxxxxx171// components/Status.tsx23type StatusProps = {4 status: "success" | "loading" | "error";5};67export const Status = (props: StatusProps) => {8 let message = "";9 if (props.status === "loading") {10 message = "Loading data...";11 } else if (props.status === "success") {12 message = "fetch data successfully";13 } else {14 message = "error occurs";15 }16 return <div>Status - {message}</div>;17};
xxxxxxxxxx251// App.tsx2import "./App.css";3import { PersonList } from "./components/PersonList";4import { Status } from "./components/Status";56function App() {7 return (8 <>9 <Status status="loading" />10 <Heading>I'm a heading.</Heading>11 </>12 );13}1415export default App;161718// components/Heading.tsx19type HeadingProps = {20 children: string;21};2223export const Heading = (props: HeadingProps) => {24 return <div>{props.children}</div>;25};
此时的children类型是React.ReactNode。
xxxxxxxxxx381// App.tsx2import "./App.css";3import { PersonList } from "./components/PersonList";4import { Status } from "./components/Status";5import { Oscar } from "./components/Oscar";67function App() {8 return (9 <>10 <Status status="loading" />11 <Heading>I'm a heading.</Heading>12 <Oscar>13 <Heading>Good good study.</Heading>14 </Oscar>15 </>16 );17}1819export default App;202122// components/Heading.tsx23type HeadingProps = {24 children: string;25};2627export const Heading = (props: HeadingProps) => {28 return <div>{props.children}</div>;29};3031// components/Oscar.tsx32type OscarProps = {33 children: React.ReactNode;34};3536export const Oscar = (props: OscarProps) => {37 return <div>{props.children}</div>;38};
可选类型。在定义的属性后面添加?。此时可以解构出可选属性,赋默认值。
xxxxxxxxxx371// App.tsx2import "./App.css";3import { Greet } from "./components/Greet";45function App() {6 return (7 <>8 <Greet name="tom anderson" isLoggedIn={true} />9 </>10 );11}1213export default App;141516// components/Greet.tsx17// 定义props类型18type GreetProps = {19 name: string;20 messageCount?: number; // 可选属性21 isLoggedIn: boolean;22};2324export const Greet = (props: GreetProps) => {25 const { messageCount = 0 } = props;26 return (27 <div>28 {props.isLoggedIn ? (29 <h2>30 Welcome {props.name}, You have {messageCount} unread messages.31 </h2>32 ) : (33 <h2>Welcome Guest</h2>34 )}35 </div>36 );37};
事件属性。
这节课学习最常见的click事件和change事件,被当作props进行传递时,类型该怎么定义。
当click事件没有传递参数,也没有返回值时,定义很简单() => void。
xxxxxxxxxx271// App.tsx2import "./App.css";3import { Button } from "./components/Button";45function App() {6 return (7 <>8 <Button9 handleClick={() => {10 console.log("clicked");11 }}12 />13 </>14 );15}1617export default App;181920// src\components\Button.tsx21type ButtonProps = {22 handleClick: () => void;23};2425export const Button = (props: ButtonProps) => {26 return <button onClick={props.handleClick}>Click</button>;27};
当需要使用到事件event参数时,此时的类型是React.MouseEvent,为了更加具体一点,可以为MouseEvent传入类型React.MouseEvent<HTMLButtonElement>,
xxxxxxxxxx271// App.tsx2import "./App.css";3import { Button } from "./components/Button";45function App() {6 return (7 <>8 <Button9 handleClick={(event) => {10 console.log("clicked", event);11 }}12 />13 </>14 );15}1617export default App;181920// src\components\Button.tsx21type ButtonProps = {22 handleClick: (event: React.MouseEvent<HTMLButtonElement>) => void;23};2425export const Button = (props: ButtonProps) => {26 return <button onClick={props.handleClick}>Click</button>;27};
使用到了高阶函数,也就是函数柯里化。这时候的event也要进行定义,即使之后用不上也要定义,但是可以在使用的时候在event前面加上_event,这样ts就会默认这个函数参数不会使用,就会不标记波浪线。
其余参数的类型定义就很简单了。
xxxxxxxxxx271// App.tsx2import "./App.css";3import { Button } from "./components/Button";45function App() {6 return (7 <>8 <Button9 handleClick={(event, id) => {10 console.log("clicked", event, id);11 }}12 />13 </>14 );15}1617export default App;181920// src\components\Button.tsx21type ButtonProps = {22 handleClick: (event: React.MouseEvent<HTMLButtonElement>, id: number) => void;23};2425export const Button = (props: ButtonProps) => {26 return <button onClick={(event) => props.handleClick(event, 1)}>Click</button>;27};
事件参数类型为React.ChangeEvent<HTMLInputElement>。
xxxxxxxxxx231// 2import "./App.css";3import { Input } from "./components/Input";45function App() {6 return (7 <>8 <Input value="" handleChange={(e) => console.log(e)} />9 </>10 );11}1213export default App;1415// src\components\Input.tsx16type InputProps = {17 value: string;18 handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;19};2021export const Input = (props: InputProps) => {22 return <input type="text" value={props.value} onChange={props.handleChange} />;23};可以看到触发了事件。input里面没有显示输入,是因为绑定了空字符串,这个不用管。

style代码被当作props传递时,应该怎么定义类型呢?
react专门提供了一个类型React.CSSProperties,用这个就可以。
xxxxxxxxxx231// App.tsx2import "./App.css";3import { Container } from "./components/Container";45function App() {6 return (7 <>8 <Container styles={{ border: "1px solid red", padding: "1rem" }} />9 </>10 );11}1213export default App;141516// src\components\Container.tsx17type ContainerProps = {18 styles: React.CSSProperties;19};2021export const Container = (props: ContainerProps) => {22 return <div style={props.styles}>Text content goes here.</div>;23};
这节课学习定义属性类型时的三个tips。
在定义组件的时候,可以直接在小括号里面解构props,这样用起来可以省略props.的代码。推荐使用这种方式。
xxxxxxxxxx231// Greet.tsx23// 定义props类型4type GreetProps = {5 name: string;6 messageCount?: number; // 可选属性7 isLoggedIn: boolean;8};910// 直接解构props11export const Greet = ({ name, messageCount = 0, isLoggedIn }: GreetProps) => {12 return (13 <div>14 {isLoggedIn ? (15 <h2>16 Welcome {name}, You have {messageCount} unread messages.17 </h2>18 ) : (19 <h2>Welcome Guest</h2>20 )}21 </div>22 );23};在上面的学习中,我们在每个组件文件中都定义了类型,这对简单组件来说可以接受,但是对于复杂、大型的组件来说,将类型定义到一个单独的文件里面,并export导出这些类型,使用时导入类型,会更好。
定义一个Person.types.ts的文件,定义并导出类型。
xxxxxxxxxx181// src\components\Person.types.ts2export type PersonProps = {3 person: {4 first: string;5 last: string;6 };7}89// src\components\Person.tsx10import type { PersonProps } from "./Person.types";1112export const Person = (props: PersonProps) => {13 return (14 <div>15 {props.person.first} {props.person.last}16 </div>17 );18};类型的重复使用。
将一些可以重复使用的类型抽离出来,这样就可以在不同的地方引入并使用了。
xxxxxxxxxx281// 将Name类型抽离出来2export type Name = {3 first: string;4 last: string;5}67export type PersonProps = {8 person: Name9}1011// 使用抽离出来的Name类型12import type { Name } from "./Person.types";1314type PersonListProps = {15 names: Name[];16};1718export const PersonList = (props: PersonListProps) => {19 return (20 <div>21 {props.names.map((name) => (22 <h2 key={name.first}>23 {name.first} {name.last}24 </h2>25 ))}26 </div>27 );28};type infer will take care of everything for simple values.
当在useState里面指定简单类型(比如说number、string、boolean等)的值时,ts会根据默认值推断出变量的类型,所以不需要特别指定类型。

没有指定类型,也没有任何ts报错。
xxxxxxxxxx141// App.tsx23import "./App.css";4import { LoggedIn } from "./components/state/LoggedIn";56function App() {7 return (8 <>9 <LoggedIn />10 </>11 );12}1314export default App;
当使用useState定义变量时,只是在未来操作的时候知道值是什么,这时候该怎么定义类型呢?
这时候就不能依靠ts的类型推断了,要指定类型,传递给useState。
xxxxxxxxxx441// App.tsx2import "./App.css";3import { User } from "./components/state/User";45function App() {6 return (7 <>8 <User />9 </>10 );11}1213export default App;141516// src\components\state\User.tsx17import { useState } from "react";1819type AuthUser = {20 name: string;21 email: string;22};2324export const User = () => {25 // 定义ts类型,使用联合类型26 const [user, setUser] = useState<AuthUser | null>(null);27 const handleLogin = () => {28 setUser({29 name: "tom",30 email: "tom@gmail.com",31 });32 };33 const handleLogout = () => {34 setUser(null);35 };36 return (37 <div>38 <button onClick={handleLogin}>Login</button>39 <button onClick={handleLogout}>Logout</button>40 <div>User name is {user?.name}</div>41 <div>User email is {user?.email}</div>42 </div>43 );44};
为什么要定义成null呢?不能定义成一个对象吗?里面的属性都为空字符串?
因为这个组件里面要判断是否登录,这个判断结果说不定要从API接口里面获取,如果已经登录了,那么就显示一部分信息(比如说欢迎界面),没有登录就显示另一部分信息(比如登录界面),要通过这个user来判断。
上面的案例中,我们会将默认值设置为null,但是有时候我们不想这么做,因为组件里面可能一直需要显示具体值。
这时候就可以使用类型断言。
xxxxxxxxxx271// 23import { useState } from "react";45type AuthUser = {6 name: string;7 email: string;8};910export const User = () => {11 // 使用类型断言12 const [user, setUser] = useState<AuthUser>({} as AuthUser);13 const handleLogin = () => {14 setUser({15 name: "tom",16 email: "tom@gmail.com",17 });18 };19 return (20 <div>21 <button onClick={handleLogin}>Login</button>22 {/* 此时就不需要使用 optional chaining operator,可选链式符 */}23 <div>User name is {user.name}</div>24 <div>User email is {user.email}</div>25 </div>26 );27};我们在使用useReducer的时候,ts会根据你传递的参数推断出类型。
const [state, dispatch] = useReducer(reducer, initialState);
那么重点就是设置reducer和initialState的类型。类型可以根据你的实际情况来写,很简单。

可以看到,只有这里需要给出类型,那么我们就可以直接定义。

可以看到useReducer里面使用时,不会要求给出类型,因为ts已经根据它的参数来推断出类型了。

并且,如果你想将state和dispatch以属性的方式传递的话,那么它们的类型也可以根据ts的提示来做,鼠标悬浮就可以看到:


上节课中,将action的type类型设置为string,这意味着可以传任意的字符串进去,虽然reducer函数里面有default来返回当前值,但是作为开发者,为什么不能更加严格一点呢?
使用联合类型来解决。

那如果reducer里面需要新增一个reset操作,不需要使用到payload,此时该怎么办呢?一般的方法就是将payload改为可选的。但此时increment和decrement的操作就需要添加判断。

有更好的方法,就是将action的类型设置为联合类型,这样ts会根据类型的不同来推断操作,此时也不需要额外的判断条件。

案例,为Box组件提供主题的上下文。
编写Box组件,非常简单:
xxxxxxxxxx41// Box.tsx2export const Box = () => {3 return <div>Theme Context</div>;4};创建提供上下文的组件:
xxxxxxxxxx251// src\components\context\theme.ts2import { createContext } from "react";34export const theme = {5 primary: {6 main: "#3f51b5",7 text: "#fff"8 },9 secondary: {10 main: "#f50057",11 text: "#ff",12 }13}1415export const ThemeContext = createContext(theme);161718// src\components\context\ThemeContext.tsx19import { theme, ThemeContext } from "./theme";2021type ThemeContextProviderProps = { children: React.ReactNode };2223export const ThemeContextProvider = ({ children }: ThemeContextProviderProps) => {24 return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;25};下一步,使用上下文组件包裹Box组件:
xxxxxxxxxx171// App.tsx23import "./App.css";4import { Box } from "./components/context/Box";5import { ThemeContextProvider } from "./components/context/ThemeContext";67function App() {8 return (9 <>10 <ThemeContextProvider>11 <Box />12 </ThemeContextProvider>13 </>14 );15}1617export default App;下一步,在Box组件里面消费上下文。
xxxxxxxxxx91// Box.tsx23import { useContext } from "react";4import { ThemeContext } from "./theme";56export const Box = () => {7 const theme = useContext(ThemeContext);8 return <div style={{ backgroundColor: theme.primary.main, color: theme.primary.text }}>Theme Context</div>;9};可以看到,我们在使用useContext的时候,没有设置ts类型,但是没有任何报错,因为此时ts会执行类型推断。

这节课来探讨,如果我们事前不知道createContext的参数值,那么这么定义类型?这个案例很复杂,不止传递了值、还传递了方法。
案例:一个User上下文组件应该提供user的相关信息和setUser方法,所以我们会在上下文组件中定义一个user状态,但是这个上下文组件在用户没有登录的情况下,是没有用户信息的,所以此时的user是null。那么在用户登录之后,user的值就是一个对象。
问题就是此时在定义createContext的时候,类型应该怎么写?
xxxxxxxxxx161// src\components\context\theme.ts23import { createContext } from "react";45export type AuthUser = {6 name: string;7 email: string;8}910type UserContextType = {11 user: AuthUser | null;12 setUser: React.Dispatch<React.SetStateAction<AuthUser | null>>13}1415// 难点就在这里,还是使用联合类型来解决,现在是null,以后可能就是UserContextType了16export const UserContext = createContext<UserContextType | null>(null)xxxxxxxxxx101// components\context\UserContext.tsx23import React, { useState } from "react";4import { type AuthUser, UserContext } from "./theme";56export const UserContextProvider = ({ children }: { children: React.ReactNode }) => {7 // 定义一个user状态,user和setUser都要传递8 const [user, setUser] = useState<AuthUser | null>(null);9 return <UserContext.Provider value={{ user, setUser }}>{children}</UserContext.Provider>;10};用UserContextProvider包裹住要传递数据的组件:
xxxxxxxxxx171// App.tsx23import "./App.css";4import { UserContextProvider } from "./components/context/UserContext";5import { User } from "./components/context/User";67function App() {8 return (9 <>10 <UserContextProvider>11 <User />12 </UserContextProvider>13 </>14 );15}1617export default App;在User组件里面使用上下文:
xxxxxxxxxx301// mycode\react-typescript-demo\src\components\context\User.tsx23import { useContext } from "react";4import { UserContext } from "./theme";56export const User = () => {7 const userContext = useContext(UserContext);8 const handleLogin = () => {9 // 为什么这里要加上判断,因为这里的userContext可能为null10 if (userContext) {11 userContext.setUser({12 name: "tom",13 email: "tom@gmail.com",14 });15 }16 };17 const handleLogout = () => {18 if (userContext) {19 userContext.setUser(null);20 }21 };22 return (23 <div>24 <button onClick={handleLogin}>Login</button>25 <button onClick={handleLogout}>Logout</button>26 <div>User name is {userContext?.user?.name}</div>27 <div>User email is {userContext?.user?.email}</div>28 </div>29 );30};

上面的代码中,在使用userContext的时候需要先判断,那么能不能不写判断呢?可以,还是按照上一个案例的方法,使用类型断言。
xxxxxxxxxx161// src\components\context\theme.ts23import { createContext } from "react";45export type AuthUser = {6 name: string;7 email: string;8}910type UserContextType = {11 user: AuthUser | null;12 setUser: React.Dispatch<React.SetStateAction<AuthUser | null>>13}1415// 使用类型断言,将UserContext的默认值设置为{}16export const UserContext = createContext<UserContextType>({} as UserContextType)这样在User组件的handleLogin和handleLogout上就不需要先判断了。为什么可以这样呢?因为在src\components\context\UserContext.tsx组件中,使用了useState来定义状态,虽然user的值可能为null,但是setUser一定是一个函数,这是定义了状态之后就确定了。
这个案例非常经典,因为这个案例把clerk提供的登录功能是怎么实现的都讲清楚了,要搞懂这个案例。
案例:要求组件一加载完成就聚焦到input框。

报错:'inputRef.current' is possibly 'null'.
那么就需要设置元素类型,并添加可选链操作符。

如果你非常确定这个组件一定会渲染,你可以给useRef添加non-null assertion,同时可以去掉可选链操作符。


这个案例是保存一个interval值。

因为window.setInterval的返回值是数值类型,所以需要将useRef的类型添加上number类型。

但还是有报错,这是因为interValRef.current可能为undefined,所以添加一个判断即可。

最后的代码:


这节课学习怎么在class定义的组件里面定义类型。

可以看到有两个地方的ts报错,为此我们需要定义state和props的ts类型,并将ts类型和组件连接起来。

xxxxxxxxxx141// App.tsx23import "./App.css";4import { Counter } from "./components/class/Counter";56function App() {7 return (8 <>9 <Counter message="The count value is" />10 </>11 );12}1314export default App;
这节课学习how to pass a component with react and typescript.
之前是将组件当作children传到另一个组件中的,类型是React.ReactNode。但是这里不同,是这样传递的<FatherComponent component={SonComponent} />,这时候该怎么定义属性的类型。
需要定义为react提供的类型React.ComponentType。如果子组件里面本身有使用props,那么需要将这个props的类型加上,React.ComponentType<SonComponentProps>。
先定义两个简单的组件:
xxxxxxxxxx161// src\components\auth\Login.tsx23export const Login = () => {4 return <div>Please login to continue</div>;5};678// src\components\auth\Profile.tsx910export type ProfileProps = {11 name: string;12};1314export const Profile = ({ name }: ProfileProps) => {15 return <div>Private Profile component.Name is {name}</div>;16};要做的就是将Profile组件当作属性传到Private组件里面去,而不是像Login组件一样直接引入使用:
xxxxxxxxxx181// src\components\auth\Private.tsx23import { Login } from "./Login";4import { type ProfileProps } from "./profile"56type PrivateProps = {7 isLoggedIn: boolean;8 component: React.ComponentType<ProfileProps>;9};1011export const Private = ({ isLoggedIn, component: Component }: PrivateProps) => {12 if (isLoggedIn) {13 // 组件标签的首字母必须是大写,所以要这样写14 return <Component name="tom" />;15 } else {16 return <Login />;17 }18};xxxxxxxxxx161// App.tsx23import "./App.css";4import { Profile } from "./components/auth/Profile";5import { Private } from "./components/auth/Private";67function App() {8 return (9 <>10 {/* 注意,传递的是 Profile ,而不是 <Profile /> */}11 <Private isLoggedIn={true} component={Profile} />12 </>13 );14}1516export default App;
但是我看到很多写法是这样的:
xxxxxxxxxx11<Private isLoggedIn={true} component={<Profile />} />传递的是一个标签,这时候该怎么定义类型呢?
这种用法就是一个组件实例,使用
React.ReactNode来定义类型。那么此时在Private里面接收到的参数,只能写成这样return Component,而不能写出这样return <Component />。为什么呢?下面会讲。
我们来深入地、形象地讲清楚——“组件类型(Component Type)” 和 “组件实例(Component Instance)” 到底有什么区别。
🧩 一、先看个例子
xxxxxxxxxx61function Profile() {2return <h1>Hello!</h1>;3}45<Profile /> // ✅ 这是一个“组件实例”6Profile // ✅ 这是一个“组件类型”🧠 二、类比理解
想象一下你在写面向对象的代码:
xxxxxxxxxx71class Dog {2bark() {3console.log("Woof!");4}5}67const myDog = new Dog(); // 🐶
名称 对应 React 中的概念 说明 Dog组件类型(Component Type) 定义了“狗是什么样”的模板 new Dog()组件实例(Component Instance) 根据模板创建出来的一只狗 同理:
React 代码 概念 解释 Profile组件类型 是一个函数或类,描述了要怎么渲染 <Profile />组件实例 运行时由 React 创建的真实元素(JSX Element) ⚙️ 三、从运行角度看区别
✅ 组件类型(Component Type)
- 是一个 函数 或 类
- React 还没执行它
- 你可以把它当作变量、参数、类型使用
xxxxxxxxxx11const MyComponent = Profile; // OK, 因为是“类型”✅ 组件实例(Component Instance)
- 是
JSX语法生成的 React 元素对象- React 已经执行了那一层函数调用的语法糖
- 它不再是函数,而是 一个对象,React 用它来渲染 UI
xxxxxxxxxx31const element = <Profile />;2// element 的实际类型是 React.ReactElement3// ReactDOM 最终用这个对象去渲染 DOM🧩 四、从 TypeScript 角度看
概念 代码示例 对应 TS 类型 组件类型 ProfileReact.ComponentType或React.FC组件实例 <Profile />React.ReactElement或React.ReactNode🧠 五、用途区别(非常关键)
使用场景 例子 用什么类型 想让别人传“组件类型”,我来控制它怎么渲染 <Private component={Profile} />React.ComponentType想让别人直接传好一个 JSX 元素 <Private component={<Profile />} />React.ReactNode🔍 六、形象比喻总结
类比 React 概念 举例 “食谱” 组件类型 Profile(描述菜怎么做)“一道做好的菜” 组件实例 <Profile />(已经做好可以上桌)你可以传“食谱”(类型),在容器组件里自己去决定什么时候、怎么做菜; 也可以直接传“做好的一道菜”(实例),容器组件只负责摆上去。
✅ 七、小结
名称 表示什么 常用类型 是否会被渲染 组件类型 函数或类组件 React.ComponentType❌ 不直接渲染 组件实例 <Component />React.ReactElement/React.ReactNode✅ 可直接渲染
在props里面怎么使用泛型。
定义一个列表组件,然后在App.tsx里面使用:
xxxxxxxxxx371// src\components\generics\List.tsx23type ListProps = {4 items: string[];5 onClick: (value: string) => void;6};78export const List = ({ items, onClick }: ListProps) => {9 return (10 <div>11 <h2>List of items</h2>12 {items.map((item, index) => {13 return (14 <div key={index} onClick={() => onClick(item)}>15 {item}16 </div>17 );18 })}19 </div>20 );21};222324// App.tsx2526import "./App.css";27import { List } from "./components/generics/List";2829function App() {30 return (31 <>32 <List items={["Batman", "Superman", "Wonder Woman"]} onClick={(item) => console.log(item)} />33 </>34 );35}3637export default App可以看到,使用没有问题。

但是当有一天,我需要将items里面的内容改为number类型,这时候就会报错:

虽然可以修改List组件里面的ts类型定义,但是太麻烦了,以后或许会有更多的类型需要添加。
这时候就可以使用泛型generics:
xxxxxxxxxx211// src\components\generics\List.tsx23type ListProps<T> = {4 items: T[];5 onClick: (value: T) => void;6};78export const List = <T extends string | number>({ items, onClick }: ListProps<T>) => {9 return (10 <div>11 <h2>List of items</h2>12 {items.map((item, index) => {13 return (14 <div key={index} onClick={() => onClick(item)}>15 {item}16 </div>17 );18 })}19 </div>20 );21};extends的用法不要忘记了。

限制属性。这节课的意思是说,如果传递了某个属性,就限制别的属性不要传递,在组件使用时就做到这一点。
xxxxxxxxxx331// src\components\restriction\RandomNumber.tsx23type RandomNumberProps = {4 value: number;5 isPositive?: boolean;6 isNegative?: boolean;7 isZero?: boolean;8};910export const RandomNumber = ({ value, isNegative, isPositive, isZero }: RandomNumberProps) => {11 return (12 <div>13 {value} {isPositive && "positive"} {isNegative && "negative"} {" "}14 {isZero && "zero"}15 </div>16 );17};181920// App.tsx2122import "./App.css";23import { RandomNumber } from "./components/restriction/RandomNumber";2425function App() {26 return (27 <>28 <RandomNumber value={10} isPositive />29 </>30 );31}3233export default App;要求,在使用RandomNumber组件时,如果传递了isPositive属性,那么isNegative和isZero属性就不能传递,这两个属性使用时也一样。
这时候要用到ts里面的never类型。
xxxxxxxxxx341// src\components\restriction\RandomNumber.tsx23type RandomNumberType = {4 value: number;5};67type PositiveProps = RandomNumberType & {8 isPositive: boolean;9 isNegative?: never;10 isZero?: never;11};1213type NegativeProps = RandomNumberType & {14 isPositive?: never;15 isNegative: boolean;16 isZero?: never;17};1819type ZeroProps = RandomNumberType & {20 isPositive?: never;21 isNegative: never;22 isZero: boolean;23};2425type RandomNumberProps = PositiveProps | NegativeProps | ZeroProps;2627export const RandomNumber = ({ value, isNegative, isPositive, isZero }: RandomNumberProps) => {28 return (29 <div>30 {value} {isPositive && "positive"} {isNegative && "negative"} {" "}31 {isZero && "zero"}32 </div>33 );34};可以看到,当写上别的属性时,会报错。

这节课学习模板字面量类型。之前我们已经学过了字符串字面量类型,目标字面量类型正是基于此。
字符串字面量类型type Direction = "north" | "south" | "east" | "west"。属于联合类型的一种。
xxxxxxxxxx201// src\components\templateLiterals.tsx23/**4 * 方向有很多种,可以定义为字符串字面量类型。像这样:5 * type PositionProps = "left-center" | "left-top" | "left-bottom" | "center" | "center-top" | "center-bottom" | "right-center" | "right-top" | "right-bottom"6 *7 * 但是这样太麻烦了,可以使用模板字面量类型8 */910type HorizontalPosition = "left" | "center" | "right";11type VerticalPosition = "top" | "center" | "bottom";1213// 这里就用到了模板字面量类型14type ToastProps = {15 position: `${HorizontalPosition}-${VerticalPosition}`;16};1718export const Toast = ({ position }: ToastProps) => {19 return <div>Toast Notification Position - {position}</div>;20};那么在App.tsx中使用的时候,就能得到正确的提示:

但是里面出现了center-center,我们只需要center就够了,怎么办呢?这时候需要使用ts的一个特性Exclude。
xxxxxxxxxx141// components\templateLiterals.tsx23type HorizontalPosition = "left" | "center" | "right";4type VerticalPosition = "top" | "center" | "bottom";56// 这里就用到了模板字面量类型7type ToastProps = {8 // 使用 Exclude排除掉 center-center ,同时使用联合类型加上 center9 position: Exclude<`${HorizontalPosition}-${VerticalPosition}`, "center-center"> | "center";10};1112export const Toast = ({ position }: ToastProps) => {13 return <div>Toast Notification Position - {position}</div>;14};这节课学习包裹原生HTML元素,创建自定义组件,同时学习接收HTML元素的属性props,定义类型。
比如说我们定义了一个Button组件,写成这样:
xxxxxxxxxx101// src\components\html\Button.tsx23type ButtonProps = {4 // 样式变体属性5 variant: "primary" | "secondary";6};78export const CustomButton = ({ variant }: ButtonProps) => {9 return <button className={`class-width-${variant}`}></button>;10};那么在App.tsx中使用的时候,可以这样写:
xxxxxxxxxx21<CustomButton variant="primary">2</CustomButton>但是如果我们加上一些原生button的属性呢?比如说在button标签之间加上文字、加上onClick事件,会怎么样呢?

会报错,报错信息是ts没有定义类型。
当然可以在自定义Button里面加上类型,但是不推荐这样的写法。因为原生button上面有很多属性,可以一次性交叉进来,React.ComponentProps<"button">。这样写:
xxxxxxxxxx41type ButtonProps = {2 // 样式变体属性3 variant: "primary" | "secondary";4} & React.ComponentProps<"button">;xxxxxxxxxx141// Button.tsx23type ButtonProps = {4 // 样式变体属性5 variant: "primary" | "secondary";6} & React.ComponentProps<"button">;78export const CustomButton = ({ variant, children, rest }: ButtonProps) => {9 return (10 <button className={`class-width-${variant}`} {rest}>11 {children}12 </button>13 );14};这样,在App.tsx中使用时,就不会有ts报错了。

同样,也可以用原生input元素创建一个react组件。
xxxxxxxxxx71// src\components\html\Input.tsx23type InputProps = React.ComponentProps<"input">;45export const Input = (props: InputProps) => {6 return <input {props} />;7};但是如果有一些属性类型,我们想限制一下,比如说children属性,只允许传递字符串过来,不允许传递组件过来,可以使用ts的Omit方法。
xxxxxxxxxx51type ButtonProps = {2 variant: "primary" | "secondary";3 // 定义children属性4 children: string;5} & Omit<React.ComponentProps<"button">, "children">;这节课学习怎么抽离一个组件的props的类型。
创建一个组件,在这个组件中,我们需要和Greet.tsx组件一样的props类型,怎么办?假设Greet里面的props类型没有导出。
使用React.ComponentProps<typeof Greet>。
xxxxxxxxxx111// src\components\html\CustomComponent.tsx23import { Greet } from "../Greet";45export const CustomComponent = (props: React.ComponentProps<typeof Greet>) => {6 return (7 <div>8 {props.isLoggedIn} {props.messageCount} {props.name}9 </div>10 );11};多态组件。在老师的react课程里面有。比如说mui里面的Typography就是一种多态组件,通过component属性可以渲染成不同的html元素。
一个多态组件是指:一个组件,可以动态地渲染成不同的底层 HTML 元素或另一个 React 组件,同时还能保持其自身的样式、逻辑和正确的类型安全。
比如说我创建了一个Text组件:
xxxxxxxxxx111// Text.tsx23type TextProps = {4 size?: "lg" | "md" | "sm";5 color?: "primary" | "secondary";6 children: React.ReactNode;7};89export const Text = ({ size, color, children }: TextProps) => {10 return <div className={`class-with-${size}-${color}`}>{children}</div>;11};可以这样使用:
xxxxxxxxxx181// App.tsx23import "./App.css";4import { Text } from "./components/polymorphic/Text";56function App() {7 return (8 <>9 <Text size="lg">Heading</Text>10 <Text size="md">Paragraph</Text>11 <Text size="sm" color="secondary">12 Label13 </Text>14 </>15 );16}1718export default App;但是有一个问题,就是渲染出来的真实html都是div,我想渲染出来的真实html分别变为h1、p、label,可以做到吗?
可以做到,通过新增一个属性来代替div,注意类型是什么。
x
1// src\components\polymorphic\Text.tsx23type TextProps = {4 size?: "lg" | "md" | "sm";5 color?: "primary" | "secondary";6 children: React.ReactNode;7 // 定义标签元素类型8 as?: React.ElementType;9};1011export const Text = ({ size, color, children, as }: TextProps) => {12 // 使用标签属性,默认值为div13 const Component = as || "div";14 return <Component className={`class-with-${size}-${color}`}>{children}</Component>;15};x
681// App.tsx23import "./App.css";4import { Text } from "./components/polymorphic/Text";56function App() {7 return (8 <>9 <Text as="h1" size="lg">10 Heading11 </Text>12 <Text as="p" size="md">13 Paragraph14 </Text>15 <Text as="label" size="sm" color="secondary">16 Label17 </Text>18 </>19 );20}2122export default App;
可以看到,渲染的真实DOM变为了h1、p、label。
但是此时组件还不能完全处理html元素的属性,比如说我在as="label"组件上面加上htmlFor属性,它还是会有类型报错:

解决办法:将html的属性加上去。记得html的属性是什么吗?上节课刚讲了,React.ComponentProps。

但是React.ComponentProps需要传递具体的html标签类型作为泛型参数,此时就可以加上。但是此时T的范围太宽泛了,所以此时需要使用extends来约束泛型,约束成什么样呢?那么因为as定义的类型是React.ElementType,所以只需要约束成这个类型即可。

上面还有一个类型报错,先不管。TextOwnProps里面的所有属性和React.ComponentType里面的属性有重复,所以把React.ComponentType里面的相关属性去掉,使用Omit。

再来处理类型报错:

可以看到,在as="label"上写htmlFor属性是正常的,但是在as="h1"上写这个属性会报错,因为h1上没有这个属性,这是相当智能了。

总结:
